
#include "../utils/Quicksorter.h"
#include "SecurityChain.h"
#include "SecurityTokenKey.h"
#include "SSHTelescope.h"
#include "SSHClientWrapper.h"

/** Automatic SSH client generation for inside the SSH telescope,
 * using existing keys from the database.
 * 
 * @param remotehost The remote host name to connect to (without
 * forwarding)
 * 
 * @param remoteport The remote port to connect to (without
 * forwarding)
 * 
 * @param localport The local port to connect to, if we are
 * forwarding. If we are not using forwarding for this step, this is
 * zero to show that a direct connection is desired.
 * 
 * @param secretkeypwd The password for decrypting the secret key in
 * the database.
 * 
 * @return The functioning SSH client, or NULL if the client failed to
 * connect. */
SSHClient *SSHTelescope::autoSSH(const char *remotehost, const int remoteport, const int localport,
   const char *secretkeypwd)
{
   SSHClient *sshcli = SSHClientWrapper::sshClientDefault(remotehost, remoteport, mp, NULL, 0,
      (localport) ? "127.0.0.1" : remotehost,
      (localport) ? localport : remoteport,
      NULL, username, networkdb, keydb, secretkeypwd);
   if ((sshcli) && (sshcli->isUp()))
   {
      return sshcli;
   } else
   {
      errprint("failed to connect to '%s:%d' (rerouted through port %d?)", remotehost, remoteport, localport);
      if (sshcli) delete sshcli;
      return NULL;
   }
}

/** This function attempts a basic TCP connection to a locally
 * accessible resource. Waits up to one second before giving up and
 * reporting the resource as unreachable.
 * 
 * @param ihostname The host name of the server to try to connect to.
 * 
 * @param iportno The port number of the server to try to connect to.
 * 
 * @return One if it is accessible (the test connection succeeded),
 * zero if it was not accessible (the test connection did not connect,
 * could not find a route, or timed out). */
int SSHTelescope::determineIfLocallyAccessible(const char *ihostname, const int iportno)
{
   telescopevalid = 0;
   apr_sockaddr_t *addrstruct;
   apr_socket_t *tsocket;
   int isinvalid = 0;
   int hassocket = 0;
   int connectsuccess = 0;
   apr_status_t status;
   status = apr_sockaddr_info_get(&addrstruct, ihostname, APR_INET, iportno, 0, mp);
   CheckAPRError(status); isinvalid |= status; if (isinvalid) goto cleanupsock;
   status = apr_socket_create(&tsocket, addrstruct->family, SOCK_STREAM, APR_PROTO_TCP, mp);
   CheckAPRError(status); isinvalid |= status; if (isinvalid) goto cleanupsock;
   hassocket = 1;
   // time to give up, 1 second
   status = apr_socket_opt_set(tsocket, APR_SO_NONBLOCK, 1);
   CheckAPRError(status); isinvalid |= status; if (isinvalid) goto cleanupsock;
   status = apr_socket_timeout_set(tsocket, 1000000);
   CheckAPRError(status); isinvalid |= status; if (isinvalid) goto cleanupsock;
   status = apr_socket_connect(tsocket, addrstruct);
   CheckAPRError(status); isinvalid |= status; if (isinvalid) goto cleanupsock;
   connectsuccess = 1;
cleanupsock:
   if (hassocket)
   {
      status = apr_socket_close(tsocket);
      CheckAPRError(status);
   }
   return (isinvalid == 0);
}

/** Sorts the set of listed hosts and ports into a vector class
 * structure that moves host names with large common suffixes with the
 * destination host to the top of the list. This ensures that similar
 * domains are expected to coincide, as each unaccessible host will
 * require a same-domain server to act as a firewall puching service.
 * 
 * @param unsorted_hosts Unsorted list of host names to use.
 * 
 * @param unsorted_ports Unsorted list of ports to use, in the same
 * ordering as the unsorted_hosts.
 * 
 * @param destination The destination host name to compare suffix
 * strings.
 * 
 * @return The list of host name port number pair with an added tuple
 * to be able to mark each of these as estblished or tried
 * connections. */
std::vector<std::pair<std::pair<std::string, int>, int> > *SSHTelescope::sortByCommonSuffix(std::vector<char *> *unsorted_hosts, std::vector<int> *unsorted_ports, const char *destination)
{
   // should probably break this on purpose for testing, otherwise
   // complicated corner cases may not get tested due to 'intelligent'
   // heuristics!
   std::vector<int> *same_chars = new std::vector<int>();
   for (int i = 0; i < unsorted_hosts->size(); i++)
   {
      int total = 0;
      const char *t_string = unsorted_hosts->at(i);
      for (int j = (strlen(t_string) - 1); j >= 0; j--)
      {
         int dst_idx = (strlen(destination) - 1) - ((strlen(t_string) - 1) - j);
         if (t_string[j] == destination[dst_idx])
            total++;
         else
            break;
      }
      same_chars->push_back(total);
   }
   int *idxs = (int *)malloc(sizeof(int)*(unsorted_hosts->size()));
   for (int i = 0; i < unsorted_hosts->size(); i++) idxs[i] = i;
   if (same_chars->size() > 0)
      Quicksorter<int>::quicksort(&(same_chars->at(0)), idxs, same_chars->size(), sizeof(int));
   std::vector<std::pair<std::pair<std::string, int>, int> > *newvector = new std::vector<std::pair<std::pair<std::string, int>, int> >();
   for (int i = (unsorted_hosts->size() - 1); i >= 0; i--)
   {
      newvector->push_back(
         std::pair<std::pair<std::string, int>, int>(
            std::pair<std::string, int>(std::string(unsorted_hosts->at(idxs[i])), unsorted_ports->at(idxs[i])), 0));
   }
   free(idxs);
   delete same_chars;
   return newvector;
}

/** Build the SSH telescope to give access to the destination.
 * 
 * @param secretkeypwd The password for the secret keys in the
 * database. */
void SSHTelescope::buildTelescope(const char *secretkeypwd)
{
   // check if the destination isn't immediately accessible...
   if (determineIfLocallyAccessible(destination_host, destination_port))
   {
      // if it is immediately accessible, the telescope isn't needed
      // so this returns with a NULL but valid telescope.
      telescopevalid = 1;
      final_local_forward = 0;
      errprint("telescope is the trivial case - none needed");
      return;
   }
   int foundroute = 0;
   std::vector<std::pair<std::pair<std::string, int>, int> > *this_trylist = NULL;
   // generate the host trying list
   std::vector<char *> *allaccessiblehosts = new std::vector<char *>();
   std::vector<int> *allaccessibleports = new std::vector<int>();
   networkdb->getAllHostsAccessibleToUserID(username, allaccessiblehosts, allaccessibleports);
   allaccessiblehosts->push_back(strdup(destination_host));
   allaccessibleports->push_back(destination_port);
   // generate the trying order
   this_trylist = sortByCommonSuffix(allaccessiblehosts, allaccessibleports, destination_host);
   StringUtils::destroyStringList(allaccessiblehosts);
   delete allaccessibleports;
   if (this_trylist->size() == 0)
   {
      errprint("not accessible, and no SSH routes to search");
      delete this_trylist;
      return;
   }
   int last_port_number = 0;
   while (foundroute == 0)
   {
      // find the next in the list that is not black listed
      int j = 0;
      for (j = 0; j < this_trylist->size(); j++)
      {
         // find hosts not tried yet
         if (this_trylist->at(j).second == LINKMARK_UNTESTED) break;
      }
      if (j == this_trylist->size())
      {
         // ran out of options - go back
         delete this_trylist;
         if (sshclients.size() == 0)
         {
            // no more choices - stop
            telescopevalid = 0;
            final_local_forward = 0;
            errprint("SSH telescope creation options exhausted: no route to host '%s:%d' or host down", destination_host, destination_port);
            return;
         } else
         {
            // shut down the previous SSH client connection
            delete sshclients.at(sshclients.size() - 1);
            sshclients.pop_back();
            // and the forwarding to get there
            delete forwardings.at(forwardings.size() - 1);
            forwardings.pop_back();
            // and pop the trylist (don't delete!)
            delete this_trylist;
            // unmark the link as established by popping the old list
            this_trylist = blacklists.at(blacklists.size() - 1);
            blacklists.pop_back();
            continue;
         }
      }
      const char *t_remote_hostname = this_trylist->at(j).first.first.c_str();
      int t_remote_portnumb = this_trylist->at(j).first.second;
      if (sshclients.size() > 0)
      {
         // create a persistent forward channel to the destination -
         // can't access it locally!
         PersistentForwardChannel *pfc = new PersistentForwardChannel(*(sshclients.at(sshclients.size() - 1)), 1, 32000, 33000, "127.0.0.1", t_remote_portnumb, t_remote_hostname);
         forwardings.push_back(pfc);
         last_port_number = pfc->getLocalListenPort();
      } else
      {
         last_port_number = 0;
      }
      // auto SSH to the destination (using the forward if needed)
      SSHClient *t_sshcli = NULL;
      // since we tested this at the start, sshclients.size() must be
      // > 0
      if (strcmp(t_remote_hostname, destination_host) == 0)
      {
         // see if the port is accessible - the final hop is not our
         // business!
         if (determineIfLocallyAccessible("127.0.0.1", last_port_number))
         {
            // we are done, the destination is accessible through here
            telescopevalid = 1;
            // need to explicitly describe the hops!
            errprint("success: can get to '%s:%d' through local port %d", destination_host, destination_port, last_port_number);
            errprint("links: %ld sshclients, %ld forwardings", sshclients.size(), forwardings.size());
            return;
         } else
         {
            if (sshclients.size() > 0)
            {
               SSHClient *last_in_stack = sshclients.at(sshclients.size() - 1);
               errprint("remote place '%s:%d' not accessible through '%s:%d' localport %d tunnel",
                  t_remote_hostname, t_remote_portnumb, last_in_stack->getRealServerName(), last_in_stack->getRealServerPort(), last_port_number);
               // we are not done, this doesn't work! fall through
               // into the NULL section of the teardown
            } else
            {
               errprint("remote place not accessible locally...");
            }
         }
      } else
      {
         // do an SSH (either direct or over the local tunnel)
         t_sshcli = autoSSH(t_remote_hostname, t_remote_portnumb, last_port_number, secretkeypwd);
      }
      if ((t_sshcli) && (t_sshcli->isUp()))
      {
         sshclients.push_back(t_sshcli);
         // if the SSH is active, then we can move onto the next round
         // of testing.  push the existing this_trylist onto the class
         // stack, and create a new trylist that omits the machines in
         // the current path. This entails simply omitting the last
         // machine from the list when copying it over.
         std::vector<std::pair<std::pair<std::string, int>, int> > *new_trylist = new std::vector<std::pair<std::pair<std::string, int>, int> >();
         for (int i = 0; i < this_trylist->size(); i++)
         {
            if (strcmp(this_trylist->at(i).first.first.c_str(), t_remote_hostname) != 0)
               new_trylist->push_back(this_trylist->at(i));
            new_trylist->push_back(this_trylist->at(i));
            new_trylist->at(i).second &= ~LINKMARK_TRIED;
         }
         new_trylist->at(j).second |= LINKMARK_ESTABLISHED;
         // push back the old trylist, if all of these fail, we may
         // have to fall back to these!
         blacklists.push_back(this_trylist);
         this_trylist = new_trylist;
      } else
      {
         // not up, the SSH client and this forwarding don't work!
         if (t_sshcli) delete t_sshcli;
         // forwarding must still be deleted if it's a final hop
         // breakage
         if (forwardings.size() > 0)
         {
            SSHClient *last_in_stack = sshclients.at(sshclients.size() - 1);
            PersistentForwardChannel *last_in_stack_pfc = forwardings.at(forwardings.size() - 1);
            errprint("remote place '%s:%d' not accessible through '%s:%d' localport %d tunnel",
               last_in_stack_pfc->getRealRemoteHost(), last_in_stack_pfc->getRealRemotePort(),
               last_in_stack->getRealServerName(), last_in_stack->getRealServerPort(), last_port_number);
            delete forwardings.at(forwardings.size() - 1);
            forwardings.pop_back();
         }
         // mark this as tried
         this_trylist->at(j).second |= LINKMARK_TRIED;
      }
   }
}

/** Constructor for the SSH telescope
 * 
 * @param iusername The delegated SSH actor ID.
 * 
 * @param inetdb The network database, used to look up host
 * information.
 * 
 * @param ikeydb The key database, to look up secret keys to access
 * the hosts.
 * 
 * @param idst_host The destination host name of the telescope tunnel.
 * 
 * @param idst_port The destination port number of the telescope
 * tunnel.
 * 
 * @param imp The memory pool whose child will be used to provide
 * memory for this class.
 * 
 * @param secretkeypwd The password to decrypt the secret keys from
 * the database. */
SSHTelescope::SSHTelescope(const char *iusername, NetworkDB &inetdb, KeyDB &ikeydb, const char *idst_host, const int idst_port, apr_pool_t *imp, const char *secretkeypwd)
{
   // should probably look up dst_host in the network db and check for
   // aliases here!
   username = NULL;
   destination_host = NULL;
   if ((iusername == NULL) || (strlen(iusername) == 0))
   {
      errprint("username provided is %s", (iusername) ? "empty" : "NULL");
      return;
   }
   if ((idst_host == NULL) || (strlen(idst_host) == 0))
   {
      errprint("destination host provided is %s", (idst_host) ? "empty" : "NULL");
      return;
   }
   apr_status_t status = apr_pool_create(&mp, imp);
   CheckAPRError(status);
   username = strdup(iusername);
   destination_host = strdup(idst_host);
   destination_port = idst_port;
   networkdb = &inetdb;
   keydb = &ikeydb;
   buildTelescope(secretkeypwd);
}

/** The destructor for the SSH telescope. Slightly complicated by the
 * fact that PersistentForwardChannel classes and SSHClient classes
 * must be destroyed in a alternating reverse order to prevent tunnel
 * collapse. */
SSHTelescope::~SSHTelescope()
{
   for (int i = 0; i < (sshclients.size()); i--)
   {
      if (((forwardings.size() - 1) - i) >= 0)
         delete forwardings.at((forwardings.size() - 1) - i);
      if (((sshclients.size() - 1) - i) >= 0)
         delete sshclients.at((sshclients.size() - 1) - i);
   }
   if (username) free(username);
   if (destination_host) free(destination_host);
}

/** Is the telescope working now?
 * 
 * @return One for yes, zero for no. */
int SSHTelescope::telescopeOkay()
{
   return telescopevalid;
}

/** Get the local port which is the end point of the tunnel if the
 * telescope is up and working. Returns zero for a locally accessible
 * resource where the tunnel infrastructure wasn't needed.
 * 
 * @return The local port needed to access the remote resource. Zero
 * means that it is down or the resource is locally accessible. Query
 * telescopeOkay() to determine which it is. */
int SSHTelescope::getLocalPort()
{
   if (forwardings.size() > 0)
      return forwardings.at(forwardings.size() - 1)->getLocalListenPort();
   else
      return 0;
}
